package com.github.facadefs.sftp;

import com.github.facadefs.FileView;
import com.github.facadefs.Selector;
import com.github.facadefs.support.SessionBasedFileSystem;
import com.github.facadefs.util.Path;
import com.github.facadefs.util.Paths;
import com.github.facadefs.util.Throwables;
import com.google.common.collect.Lists;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * 基于 JSCH 的 SFTP 文件系统
 *
 * @author vacoor
 * @since 1.0
 */
@Slf4j
@Getter
@Setter
public class JschSftpFileSystem extends SessionBasedFileSystem<ChannelSftp> {
    private static final int NO_SUCH_FILE_ID = 2;

    private String host;
    private int port;
    private String username;
    private String password;
    private byte[] privateKey;
    private byte[] passphrase;

    private String chroot;

    @Deprecated
    public JschSftpFileSystem() {
    }

    public JschSftpFileSystem(final String host, final int port,
                              final String username, final String password) {
        this(host, port, username, password, null, null, null);
    }

    public JschSftpFileSystem(final String host, final int port,
                              final String username, final byte[] privateKey) {
        this(host, port, username, null, privateKey, null, null);
    }

    public JschSftpFileSystem(final String host, final int port,
                              final String username, final byte[] privateKey, final byte[] passphrase) {
        this(host, port, username, null, privateKey, passphrase, null);
    }

    public JschSftpFileSystem(final String host, final int port,
                              final String username, final String password, final byte[] privateKey) {
        this(host, port, username, password, privateKey, null, null);
    }

    public JschSftpFileSystem(final String host, final int port,
                              final String username, final String password,
                              final byte[] privateKey, final byte[] passphrase) {
        this(host, port, username, password, privateKey, passphrase, null);
    }

    public JschSftpFileSystem(final String host, final int port,
                              final String username, final String password,
                              final byte[] privateKey, final byte[] passphrase, final String chroot) {
        this.host = host;
        this.port = port;
        this.username = username;
        this.password = password;
        this.privateKey = privateKey;
        this.passphrase = passphrase;
        this.chroot = chroot;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutputStream create(final String path, final String contentType, final boolean override) throws IOException {
        final ChannelSftp sftp = this.connect();
        try {
            return write(sftp, path, override, new SftpProgressMonitorAdapter() {
                @Override
                public void end() {
                    disconnect(sftp);
                }
            });
        } catch (final Throwable ex) {
            disconnect(sftp);
            return Throwables.rethrowIOException(ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ChannelSftp connect() throws IOException {
        try {
            final JSch jsch = new JSch();
            if (null != privateKey && privateKey.length > 0) {
                jsch.addIdentity(username, privateKey, null, passphrase);
            }

            final Session session = jsch.getSession(username, host, port);
            session.setConfig("StrictHostKeyChecking", "no");
            /*
            session.setConfig("compression.s2c", "zlib,none");
            session.setConfig("compression.c2s", "zlib,none");
            */
            // session.setTimeout(3 * 1000);
            session.setPassword(password);

            // session.connect(10 * 1000);
            session.connect();

            final ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
            sftp.connect();
            return sftp;
        } catch (final JSchException ex) {
            throw new IOException(String.format("Server - %s refused connection on port - %s: %s", host, port, ex.getMessage()), ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void disconnect(final ChannelSftp sftp) {
        if (null == sftp) {
            return;
        }
        try {
            try {
                if (!sftp.isClosed()) {
                    sftp.disconnect();
                }
            } finally {
                final Session session = sftp.getSession();
                if (session.isConnected()) {
                    session.disconnect();
                }
            }
        } catch (final JSchException skip) {
            log.warn("Server - {}:{} disconnect error: {}", host, port, skip.getMessage());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected FileView stat(final ChannelSftp sftp, final String path) throws IOException {
        try {
            final String absPath = this.toAbsolutePath(path);
            final SftpATTRS attrs = stat0(sftp, absPath);
            return null != attrs ? toView(sftp, absPath, null, attrs) : null;
        } catch (final SftpException e) {
            throw new IOException(e);
        }
    }

    private SftpATTRS stat0(final ChannelSftp sftp, final String absPath) throws SftpException {
        try {
            return sftp.stat(absPath);
        } catch (final SftpException e) {
            if (NO_SUCH_FILE_ID == e.id) {
                return null;
            }
            throw e;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected FileView[] ls(final ChannelSftp sftp, final String path) throws IOException {
        final String absPath = this.toAbsolutePath(path);
        try {
            final SftpATTRS stat = stat0(sftp, absPath);
            if (null == stat) {
                return new FileView[0];
            }
            if (!stat.isDir()) {
                return new FileView[]{toView(sftp, absPath, null, stat)};
            }

            final List<ChannelSftp.LsEntry> entries = Lists.newLinkedList();
            sftp.ls(absPath, new ChannelSftp.LsEntrySelector() {
                @Override
                public int select(final ChannelSftp.LsEntry entry) {
                    final String filename = entry.getFilename();
                    if (!Paths.isCurrentDirName(filename) && !Paths.isParentDirName(filename)) {
                        entries.add(entry);
                    }
                    return CONTINUE;
                }
            });

            // 不能在未读取完前再次读取.
            final FileView[] views = new FileView[entries.size()];
            for (int i = 0; i < entries.size(); i++) {
                final ChannelSftp.LsEntry entry = entries.get(i);
                final String filename = entry.getFilename();
                final String longname = entry.getLongname();
                final SftpATTRS attrs = entry.getAttrs();
                views[i] = (toView(sftp, Path.get(absPath, filename).getPath(), longname, attrs));
            }
            return views;
        } catch (final SftpException e) {
            throw new IOException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void ls(final ChannelSftp sftp, final String path, final Selector selector) throws IOException {
        final String absPath = this.toAbsolutePath(path);
        try {
            final SftpATTRS stat = stat0(sftp, absPath);
            if (null == stat) {
                return;
            }
            if (!stat.isDir()) {
                selector.select(toView(sftp, absPath, null, stat));
                return;
            }

            sftp.ls(absPath, new ChannelSftp.LsEntrySelector() {
                @Override
                public int select(final ChannelSftp.LsEntry entry) {
                    final String filename = entry.getFilename();
                    if (!Paths.isCurrentDirName(filename) && !Paths.isParentDirName(filename)) {
                        final FileView view = toView(sftp, toAbsolutePath(absPath, filename), entry.getLongname(), entry.getAttrs());
                        return Selector.CONTINUE == selector.select(view) ? CONTINUE : BREAK;
                    }
                    return CONTINUE;
                }
            });
        } catch (final SftpException e) {
            throw new IOException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean mkdirs(final ChannelSftp sftp, final String path) throws IOException {
        final String absPath = this.toAbsolutePath(path);
        final StringTokenizer tokenizer = new StringTokenizer(absPath, Paths.PATH_SEPARATOR, true);
        try {
            String pathToUse = "";
            while (tokenizer.hasMoreElements()) {
                pathToUse = pathToUse + tokenizer.nextElement();

                final SftpATTRS stat = stat0(sftp, pathToUse);
                if (null == stat) {
                    try {
                        sftp.mkdir(pathToUse);
                    } catch (final SftpException ex2) {
                        // ignore
                        return false;
                    }
                } else if (!stat.isDir()) {
                    throw new IOException("File already exists and is not directory: " + pathToUse);
                }
            }
            return true;
        } catch (final SftpException e) {
            throw new IOException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    protected void rm(final ChannelSftp sftp, final String path, final boolean recurse) throws IOException {
        final String absPath = this.toAbsolutePath(path);
        try {
            final SftpATTRS stat = stat0(sftp, absPath);
            if (null == stat) {
                return;
            }
            if (stat.isDir()) {
                final Vector<ChannelSftp.LsEntry> entries = sftp.ls(absPath);
                if (!recurse && 0 < entries.size()) {
                    throw new IOException("You cannot delete non-empty directory, use recurse=true to override");
                }

                for (final ChannelSftp.LsEntry entry : entries) {
                    final String name = entry.getFilename();
                    if (Paths.isCurrentDirName(name) || Paths.isParentDirName(name)) {
                        continue;
                    }
                    this.rm(sftp, toAbsolutePath(absPath, name), recurse);
                }
                sftp.rmdir(absPath);
            } else {
                sftp.rm(absPath);
            }
        } catch (final SftpException e) {
            throw new IOException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void rename(final ChannelSftp sftp, final String oldPath, final String newPath) throws IOException {
        final String absSrc = this.toAbsolutePath(oldPath);
        final String absDst = this.toAbsolutePath(newPath);
        try {
            // FIXME 可能是目录重命名
            sftp.rename(absSrc, absDst);
        } catch (final SftpException e) {
            throw new IOException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected InputStream open(final ChannelSftp sftp, final String path) throws IOException {
        final String absPath = this.toAbsolutePath(path);
        try {
            final SftpATTRS stat = stat0(sftp, absPath);
            if (null == stat) {
                return null;
            }
            if (stat.isDir()) {
                throw new IOException("Can't open " + path + " because it is a directory");
            }
            return sftp.get(absPath);
        } catch (final SftpException e) {
            throw new IOException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected OutputStream write(final ChannelSftp sftp, final String path,
                                 final String contentType, final boolean override) throws IOException {
        return write(sftp, path, override, null);
    }

    private OutputStream write(final ChannelSftp sftp, final String path,
                               final boolean override, final SftpProgressMonitor monitor) throws IOException {
        final int offset = 0;
        final String absPath = this.toAbsolutePath(path);
        try {
            final SftpATTRS stat = stat0(sftp, absPath);
            if (null != stat) {
                if (!override) {
                    throw new IOException("File already exists: " + absPath);
                } else if (stat.isDir()) {
                    throw new IOException("Can't override " + path + " because it is a directory");
                }
            } else {
                final int i = absPath.lastIndexOf(Paths.PATH_SEPARATOR);
                final String absParentPath = i <= 0 ? null : absPath.substring(0, i);
                if (null != absParentPath && !this.mkdirs(sftp, absParentPath)) {
                    throw new IOException("mkdirs failed to create: " + absParentPath);
                }
            }
            return sftp.put(absPath, monitor, ChannelSftp.OVERWRITE, offset);
        } catch (final SftpException e) {
            throw new IOException(e);
        }
    }

    private String toAbsolutePath(final String path) {
        return toAbsolutePath(chroot, path);
    }

    private String toAbsolutePath(final String base, final String path) {
        return Paths.toAbsolutePath(base, path);
    }

    private String toRelativePath(final String path) {
        return Paths.toRelativePath(chroot, path);
    }

    protected FileView toView(final ChannelSftp sftp, final String absPath, final String longname, final SftpATTRS attrs) {
        final long length = attrs.getSize();
        final boolean dir = attrs.isDir();
        final long lastAccessTime = attrs.getATime() * 1000L;
        final long lastModifiedTime = attrs.getMTime() * 1000L;
        final int permissions = attrs.getPermissions();
        final int uid = attrs.getUId();
        final int gid = attrs.getGId();
        final boolean isLink = attrs.isLink();
        String symlink = null;
        if (isLink) {
            try {
                // sftp.readlink(path);    // 链接内容, 可能是相对路径
                symlink = sftp.realpath(absPath);    // 链接的真实绝对路径
            } catch (SftpException e) {
                symlink = null;
            }
        }
        final String[] info = Utils.parseUserAndGroup(longname);
        final String owner = 1 < info.length ? info[0] : String.valueOf(uid);
        final String group = 1 < info.length ? info[1] : String.valueOf(gid);
        return new FileView(toRelativePath(absPath), length, dir, lastModifiedTime, lastAccessTime, permissions, owner, group, symlink);
    }

    protected static class SftpProgressMonitorAdapter implements SftpProgressMonitor {
        @Override
        public void init(final int i, final String s, final String s1, final long l) {
        }

        @Override
        public boolean count(final long readed) {
            return true;
        }

        @Override
        public void end() {
        }

    }
}
